{{>licenseInfo}}
package {{invokerPackage}};

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
{{#openApiNullable}}
import org.openapitools.jackson.nullable.JsonNullableModule;
{{/openApiNullable}}

import java.io.InputStream;
import java.io.IOException;
{{#useGzipFeature}}
import java.io.ByteArrayOutputStream;
{{/useGzipFeature}}
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpConnectTimeoutException;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.Optional;
import java.util.zip.GZIPInputStream;
{{#useGzipFeature}}
import java.util.function.Supplier;
import java.util.Objects;
import java.util.zip.GZIPOutputStream;
{{/useGzipFeature}}
import java.util.stream.Collectors;
{{#useUnaryInterceptor}}
import java.util.function.UnaryOperator;
{{/useUnaryInterceptor}}

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Configuration and utility class for API clients.
 *
 * <p>This class can be constructed and modified, then used to instantiate the
 * various API classes. The API classes use the settings in this class to
 * configure themselves, but otherwise do not store a link to this class.</p>
 *
 * <p>This class is mutable and not synchronized, so it is not thread-safe.
 * The API classes generated from this are immutable and thread-safe.</p>
 *
 * <p>The setter methods of this class return the current object to facilitate
 * a fluent style of configuration.</p>
 */
{{>generatedAnnotation}}

public class ApiClient {

  protected HttpClient.Builder builder;
  protected ObjectMapper mapper;
  protected String scheme;
  protected String host;
  protected int port;
  protected String basePath;
  protected Consumer<HttpRequest.Builder> interceptor;
{{#useUnaryInterceptor}}
  protected UnaryOperator<HttpResponse<InputStream>> responseInterceptor;
  protected UnaryOperator<HttpResponse<InputStream>> asyncResponseInterceptor;
{{/useUnaryInterceptor}}
{{^useUnaryInterceptor}}
  protected Consumer<HttpResponse<InputStream>> responseInterceptor;
  protected Consumer<HttpResponse<InputStream>> asyncResponseInterceptor;
{{/useUnaryInterceptor}}
  protected Duration readTimeout;
  protected Duration connectTimeout;

  public static String valueToString(Object value) {
    if (value == null) {
      return "";
    }
    if (value instanceof OffsetDateTime) {
      return ((OffsetDateTime) value).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
    }
    return value.toString();
  }

  /**
   * URL encode a string in the UTF-8 encoding.
   *
   * @param s String to encode.
   * @return URL-encoded representation of the input string.
   */
  public static String urlEncode(String s) {
    return URLEncoder.encode(s, UTF_8).replaceAll("\\+", "%20");
  }

  /**
   * Convert a URL query name/value parameter to a list of encoded {@link Pair}
   * objects.
   *
   * <p>The value can be null, in which case an empty list is returned.</p>
   *
   * @param name The query name parameter.
   * @param value The query value, which may not be a collection but may be
   *              null.
   * @return A singleton list of the {@link Pair} objects representing the input
   * parameters, which is encoded for use in a URL. If the value is null, an
   * empty list is returned.
   */
  public static List<Pair> parameterToPairs(String name, Object value) {
    if (name == null || name.isEmpty() || value == null) {
      return Collections.emptyList();
    }
    return Collections.singletonList(new Pair(urlEncode(name), urlEncode(valueToString(value))));
  }

  /**
   * Convert a URL query name/collection parameter to a list of encoded
   * {@link Pair} objects.
   *
   * @param collectionFormat The swagger collectionFormat string (csv, tsv, etc).
   * @param name The query name parameter.
   * @param values A collection of values for the given query name, which may be
   *               null.
   * @return A list of {@link Pair} objects representing the input parameters,
   * which is encoded for use in a URL. If the values collection is null, an
   * empty list is returned.
   */
  public static List<Pair> parameterToPairs(
      String collectionFormat, String name, Collection<?> values) {
    if (name == null || name.isEmpty() || values == null || values.isEmpty()) {
      return Collections.emptyList();
    }

    // get the collection format (default: csv)
    String format = collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat;

    // create the params based on the collection format
    if ("multi".equals(format)) {
      return values.stream()
          .map(value -> new Pair(urlEncode(name), urlEncode(valueToString(value))))
          .collect(Collectors.toList());
    }

    String delimiter;
    switch(format) {
      case "csv":
        delimiter = urlEncode(",");
        break;
      case "ssv":
        delimiter = urlEncode(" ");
        break;
      case "tsv":
        delimiter = urlEncode("\t");
        break;
      case "pipes":
        delimiter = urlEncode("|");
        break;
      default:
        throw new IllegalArgumentException("Illegal collection format: " + collectionFormat);
    }

    StringJoiner joiner = new StringJoiner(delimiter);
    for (Object value : values) {
      joiner.add(urlEncode(valueToString(value)));
    }

    return Collections.singletonList(new Pair(urlEncode(name), joiner.toString()));
  }

  /**
   * Create an instance of ApiClient.
   */
  public ApiClient() {
    this.builder = createDefaultHttpClientBuilder();
    this.mapper = createDefaultObjectMapper();
    updateBaseUri("{{{basePath}}}");
    interceptor = null;
    readTimeout = null;
    connectTimeout = null;
    responseInterceptor = null;
    asyncResponseInterceptor = null;
  }

  /**
   * Create an instance of ApiClient.
   *
   * @param builder Http client builder.
   * @param mapper Object mapper.
   * @param baseUri Base URI
   */
  public ApiClient(HttpClient.Builder builder, ObjectMapper mapper, String baseUri) {
    this.builder = builder;
    this.mapper = mapper;
    updateBaseUri(baseUri != null ? baseUri : "{{{basePath}}}");
    interceptor = null;
    readTimeout = null;
    connectTimeout = null;
    responseInterceptor = null;
    asyncResponseInterceptor = null;
  }

  public static ObjectMapper createDefaultObjectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
    mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
    mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
    mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
    mapper.registerModule(new JavaTimeModule());
    {{#openApiNullable}}
    mapper.registerModule(new JsonNullableModule());
    {{/openApiNullable}}
    mapper.registerModule(new RFC3339JavaTimeModule());
    return mapper;
  }

  protected final String getDefaultBaseUri() {
    return basePath;
  }

  public static HttpClient.Builder createDefaultHttpClientBuilder() {
    return HttpClient.newBuilder();
  }

  public final void updateBaseUri(String baseUri) {
    URI uri = URI.create(baseUri);
    scheme = uri.getScheme();
    host = uri.getHost();
    port = uri.getPort();
    basePath = uri.getRawPath();
  }

  /**
   * Set a custom {@link HttpClient.Builder} object to use when creating the
   * {@link HttpClient} that is used by the API client.
   *
   * @param builder Custom client builder.
   * @return This object.
   */
  public ApiClient setHttpClientBuilder(HttpClient.Builder builder) {
    this.builder = builder;
    return this;
  }

  /**
   * Get an {@link HttpClient} based on the current {@link HttpClient.Builder}.
   *
   * <p>The returned object is immutable and thread-safe.</p>
   *
   * @return The HTTP client.
   */
  public HttpClient getHttpClient() {
    return builder.build();
  }

  /**
   * Set a custom {@link ObjectMapper} to serialize and deserialize the request
   * and response bodies.
   *
   * @param mapper Custom object mapper.
   * @return This object.
   */
  public ApiClient setObjectMapper(ObjectMapper mapper) {
    this.mapper = mapper;
    return this;
  }

  /**
   * Get a copy of the current {@link ObjectMapper}.
   *
   * @return A copy of the current object mapper.
   */
  public ObjectMapper getObjectMapper() {
    return mapper.copy();
  }

  /**
   * Set a custom host name for the target service.
   *
   * @param host The host name of the target service.
   * @return This object.
   */
  public ApiClient setHost(String host) {
    this.host = host;
    return this;
  }

  /**
   * Set a custom port number for the target service.
   *
   * @param port The port of the target service. Set this to -1 to reset the
   *             value to the default for the scheme.
   * @return This object.
   */
  public ApiClient setPort(int port) {
    this.port = port;
    return this;
  }

  /**
   * Set a custom base path for the target service, for example '/v2'.
   *
   * @param basePath The base path against which the rest of the path is
   *                 resolved.
   * @return This object.
   */
  public ApiClient setBasePath(String basePath) {
    this.basePath = basePath;
    return this;
  }

  /**
   * Get the base URI to resolve the endpoint paths against.
   *
   * @return The complete base URI that the rest of the API parameters are
   * resolved against.
   */
  public String getBaseUri() {
    return scheme + "://" + host + (port == -1 ? "" : ":" + port) + basePath;
  }

  /**
   * Set a custom scheme for the target service, for example 'https'.
   *
   * @param scheme The scheme of the target service
   * @return This object.
   */
  public ApiClient setScheme(String scheme){
    this.scheme = scheme;
    return this;
  }

  /**
   * Set a custom request interceptor.
   *
   * <p>A request interceptor is a mechanism for altering each request before it
   * is sent. After the request has been fully configured but not yet built, the
   * request builder is passed into this function for further modification,
   * after which it is sent out.</p>
   *
   * <p>This is useful for altering the requests in a custom manner, such as
   * adding headers. It could also be used for logging and monitoring.</p>
   *
   * @param interceptor A function invoked before creating each request. A value
   *                    of null resets the interceptor to a no-op.
   * @return This object.
   */
  public ApiClient setRequestInterceptor(Consumer<HttpRequest.Builder> interceptor) {
    this.interceptor = interceptor;
    return this;
  }

  /**
   * Get the custom interceptor.
   *
   * @return The custom interceptor that was set, or null if there isn't any.
   */
  public Consumer<HttpRequest.Builder> getRequestInterceptor() {
    return interceptor;
  }

  /**
   * Set a custom response interceptor.
   *
   * <p>This is useful for logging, monitoring or extraction of header variables</p>
   *{{#useUnaryInterceptor}} <p>If you are using the UnaryInterceptor you can even manipulate the response to a certain degree</p>{{/useUnaryInterceptor}}
   * @param interceptor A function invoked before creating each request. A value
   *                    of null resets the interceptor to a no-op.
   * @return This object.
   */
  public ApiClient setResponseInterceptor({{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}<HttpResponse<InputStream>> interceptor) {
    this.responseInterceptor = interceptor;
    return this;
  }

 /**
   * Get the custom response interceptor.
   *
   * @return The custom interceptor that was set, or null if there isn't any.
   */
  public {{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}<HttpResponse<InputStream>> getResponseInterceptor() {
    return responseInterceptor;
  }

  /**
   * Set a custom async response interceptor. Use this interceptor when asyncNative is set to 'true'.
   *
   * <p>This is useful for logging, monitoring or extraction of header variables</p>
   *{{#useUnaryInterceptor}} <p>If you are using the UnaryInterceptor you can even manipulate the response to a certain degree</p>{{/useUnaryInterceptor}}
   * @param interceptor A function invoked before creating each request. A value
   *                    of null resets the interceptor to a no-op.
   * @return This object.
   */
  public ApiClient setAsyncResponseInterceptor({{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}<HttpResponse<InputStream>> interceptor) {
    this.asyncResponseInterceptor = interceptor;
    return this;
  }

 /**
   * Get the custom async response interceptor. Use this interceptor when asyncNative is set to 'true'.
   *
   * @return The custom interceptor that was set, or null if there isn't any.
   */
  public {{#useUnaryInterceptor}}UnaryOperator{{/useUnaryInterceptor}}{{^useUnaryInterceptor}}Consumer{{/useUnaryInterceptor}}<HttpResponse<InputStream>> getAsyncResponseInterceptor() {
    return asyncResponseInterceptor;
  }

  /**
   * Set the read timeout for the http client.
   *
   * <p>This is the value used by default for each request, though it can be
   * overridden on a per-request basis with a request interceptor.</p>
   *
   * @param readTimeout The read timeout used by default by the http client.
   *                    Setting this value to null resets the timeout to an
   *                    effectively infinite value.
   * @return This object.
   */
  public ApiClient setReadTimeout(Duration readTimeout) {
    this.readTimeout = readTimeout;
    return this;
  }

  /**
   * Get the read timeout that was set.
   *
   * @return The read timeout, or null if no timeout was set. Null represents
   * an infinite wait time.
   */
  public Duration getReadTimeout() {
    return readTimeout;
  }
  /**
   * Sets the connect timeout (in milliseconds) for the http client.
   *
   * <p> In the case where a new connection needs to be established, if
   * the connection cannot be established within the given {@code
   * duration}, then {@link HttpClient#send(HttpRequest,BodyHandler)
   * HttpClient::send} throws an {@link HttpConnectTimeoutException}, or
   * {@link HttpClient#sendAsync(HttpRequest,BodyHandler)
   * HttpClient::sendAsync} completes exceptionally with an
   * {@code HttpConnectTimeoutException}. If a new connection does not
   * need to be established, for example if a connection can be reused
   * from a previous request, then this timeout duration has no effect.
   *
   * @param connectTimeout connection timeout in milliseconds
   *
   * @return This object.
   */
  public ApiClient setConnectTimeout(Duration connectTimeout) {
    this.connectTimeout = connectTimeout;
    this.builder.connectTimeout(connectTimeout);
    return this;
  }

  /**
   * Get connection timeout (in milliseconds).
   *
   * @return Timeout in milliseconds
   */
  public Duration getConnectTimeout() {
    return connectTimeout;
  }

  /**
   * Returns the response body InputStream, transparently decoding gzip-compressed
   * payloads when the server sets {@code Content-Encoding: gzip}.
   *
   * @param response HTTP response whose body should be consumed
   * @return Original or decompressed InputStream for the response body
   * @throws IOException if the response body cannot be accessed or wrapping fails
   */
  public static InputStream getResponseBody(HttpResponse<InputStream> response) throws IOException {
    if (response == null) {
      return null;
    }
    InputStream body = response.body();
    if (body == null) {
      return null;
    }
    Optional<String> encoding = response.headers().firstValue("Content-Encoding");
    if (encoding.isPresent()) {
      for (String token : encoding.get().split(",")) {
        if ("gzip".equalsIgnoreCase(token.trim())) {
          return new GZIPInputStream(body);
        }
      }
    }
    return body;
  }

{{#useGzipFeature}}
  /**
   * Wraps a request body supplier with a streaming GZIP compressor so large payloads
   * can be sent without buffering the entire contents in memory.
   *
   * @param bodySupplier Supplies the original request body InputStream
   * @return BodyPublisher that emits gzip-compressed bytes from the supplied stream
   */
  public static HttpRequest.BodyPublisher gzipRequestBody(Supplier<InputStream> bodySupplier) {
    Objects.requireNonNull(bodySupplier, "bodySupplier must not be null");
    return HttpRequest.BodyPublishers.ofInputStream(() -> new GzipCompressingInputStream(bodySupplier));
  }

  private static final class GzipCompressingInputStream extends InputStream {
    private final Supplier<InputStream> supplier;
    private final byte[] readBuffer = new byte[8192];
    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    private InputStream source;
    private GZIPOutputStream gzipStream;
    private byte[] currentChunk = new byte[0];
    private int chunkPosition = 0;
    private boolean finished = false;

    private GzipCompressingInputStream(Supplier<InputStream> supplier) {
      this.supplier = Objects.requireNonNull(supplier, "bodySupplier must not be null");
    }

    private void ensureInitialized() throws IOException {
      if (source == null) {
        source = Objects.requireNonNull(supplier.get(), "bodySupplier returned null InputStream");
        gzipStream = new GZIPOutputStream(buffer, true);
      }
    }

    private boolean fillBuffer() throws IOException {
      ensureInitialized();
      while (chunkPosition >= currentChunk.length) {
        buffer.reset();
        if (finished) {
          return false;
        }
        int bytesRead = source.read(readBuffer);
        if (bytesRead == -1) {
          gzipStream.finish();
          gzipStream.close();
          source.close();
          finished = true;
        } else {
          gzipStream.write(readBuffer, 0, bytesRead);
          gzipStream.flush();
        }
        currentChunk = buffer.toByteArray();
        chunkPosition = 0;
        if (currentChunk.length == 0 && !finished) {
          continue;
        }
        if (currentChunk.length == 0 && finished) {
          return false;
        }
        return true;
      }
      return true;
    }

    @Override
    public int read() throws IOException {
      if (!fillBuffer()) {
        return -1;
      }
      return currentChunk[chunkPosition++] & 0xFF;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
      if (!fillBuffer()) {
        return -1;
      }
      int bytesToCopy = Math.min(len, currentChunk.length - chunkPosition);
      System.arraycopy(currentChunk, chunkPosition, b, off, bytesToCopy);
      chunkPosition += bytesToCopy;
      return bytesToCopy;
    }

    @Override
    public void close() throws IOException {
      IOException exception = null;
      if (source != null) {
        try {
          source.close();
        } catch (IOException e) {
          exception = e;
        } finally {
          source = null;
        }
      }
      if (gzipStream != null) {
        try {
          gzipStream.close();
        } catch (IOException e) {
          exception = exception == null ? e : exception;
        } finally {
          gzipStream = null;
        }
      }
      if (exception != null) {
        throw exception;
      }
    }
  }
{{/useGzipFeature}}
}
